/*
 <This Java Class is part of the jMusic API version 1.5, March 2004.>
 
 Copyright (C) 2000 Andrew Sorensen & Andrew Brown
 
 This program is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 2 of the License, or any
 later version.
 This program is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 GNU General Public License for more details.
 You should have received a copy of the GNU General Public License
 along with this program; if not, write to the Free Software
 Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */
package jm.midi;

import javax.sound.midi.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;

/**
 * This class provides an easy interface to the MIDI input and output facilities
 * in JavaSound. Classes that extend this abstract class will need to implement
 * the handleMidiInput() method to respond to MIDI data from external sources.
 * MIDI data can be sent using the senMidiOutput() method. The default constructor
 * will prompt the user (via  a GUI interface) to choose the MIDI ports to use. To
 * prevent this from hapenning pass the boolean false as an argument to the
 * constructor using the super call from th extending class. A third constructor
 * can be used if you know the ports you want to use, it accepts the input and
 * output port IDs as arguments.
 *
 * @author Andrew Brown
 * @version 1.0, August 2004
 */

public abstract class MidiCommunication implements Receiver {
    private Receiver midiReceiver;
    private boolean waitingToSetup = true;

    /**
     * A constructor that automatically prompts for the MIDI i/o ports.
     */
    public MidiCommunication() {
        setupMidiInput();
        while (waitingToSetup) {
            try {
                Thread.sleep(100);
            } catch (Exception e) {
            }
        }
    }

    /**
     * A constructor that supresses the automatic prompting of ports,
     * allowing the programmer to call midiSetup when it suits them.
     * Warning: Attempting to send a MIDI message before setting up ports will
     * cause a NullPointerException.
     */
    public MidiCommunication(boolean prompt) {
        if (prompt) {
            setupMidiInput();
            while (waitingToSetup) {
                try {
                    Thread.sleep(100);
                } catch (Exception e) {
                }
            }
        }
    }

    /**
     * A constructor that automatic assigns MIDI input and output to the ports
     * you define. Use this as a convenient way of setting up MIDI IO if your
     * application is running on a hardware setup that never (or rarely) changes.
     * Warning: Attempting to assigning unavailible devicePortID's will cause
     * a NullPointerException.
     *
     * @param inputDeviceID  The MIDI port number to use for input to Java.
     * @param outputDeviceID The MIDI port number to use for output from Java.
     */
    public MidiCommunication(int inputDeviceID, int outputDeviceID) {
        final MidiDevice.Info[] info = MidiSystem.getMidiDeviceInfo();
        setMidiInputSelection(inputDeviceID, info);
        setMidiOutputSelection(outputDeviceID, info);
    }

    /**
     * Choose the MIDI ports to use. This method will prompt you with a list of
     * the availible MIDI inputs and outputs, double click on the desired ports.
     */
    public void midiSetup() {
        setupMidiInput();
    }

    /**
     * Handle external MIDI events sent to this computer. This method should be
     * overridden in the class that implements this interface, and the messages
     * handled as appropriate.
     *
     * @param status  The MIDI status type. 11 = Controller, ? = Note on, etc.
     * @param channel The MIDi channel this message is sent on.
     * @param data1   The first byte of data in the message. Often controller number of Note pitch.
     * @param data2   The second byte of data. Usually the controller value or Note velocity.
     */
    public abstract void handleMidiInput(int status, int channel, int data1, int data2);

    /**
     * Construct a MIDI message and pass to the designated MIDi output.
     *
     * @param status  The MIDI status type. 11 = Controller, 144 = Note on, 128 = Note Off, 192 = Program Change, etc.
     * @param channel The MIDi channel this message is sent on.
     * @param data1   The first byte of data in the message. Often controller number of Note pitch.
     * @param data2   The second byte of data. Usually the controller value or Note velocity.
     */
    public void sendMidiOutput(int status, int channel, int data1, int data2) {
        try {
            ShortMessage message = new ShortMessage();
            message.setMessage(status, channel, data1, data2);
            this.midiReceiver.send(message, -1L);
        } catch (javax.sound.midi.InvalidMidiDataException e) {
        }
        ;
    }

    /**
     * The method required by the recieve interface - It has a stupid name so we
     * pass data for short messages to handleMidiInput (a better name).
     */
    // This method is based on test code in Plumbstone - OSX Core MIDI package.
    public void send(MidiMessage message, long timeStamp) {
        // Get the message bytes
        byte[] m = message.getMessage();
        int status = message.getStatus();
        // Is it a short message ?
        if (message instanceof ShortMessage) {
            // Get command and channel data
            int type = (m[0] & 0xFF) >> 4;
            int channel = m[0] & 0xF;
            int data1 = m[1];
            int data2 = -1;
            if (m.length > 2) {
                data2 = m[2];
            }

            // Is it a channel message?
            if (type != 15) {
                handleMidiInput(status - channel, channel, data1, data2);
            } else {
                // System message - some need special handling due to vast quanitity
                if (status == ShortMessage.TIMING_CLOCK) {
                    System.out.print("MIDI Clock message");
                } else if (status == ShortMessage.ACTIVE_SENSING) {
                    System.out.print("MIDI Active sensing message");
                } else {
                    // Some other system message - just print status byte
                    System.out.print("A non-identified MIDI system message " + status);
                }
            }
        }

        // System exclusive message?
        else if (message instanceof SysexMessage) {
            // Print out sysex message
            System.out.println();
            System.out.print("Sysex MIDI message <<");
            for (int i = 0; i < m.length; i++) {
                System.out.print(" " + m[i]);
            }
            System.out.println(">>");
        }

        // Meta message (probably shouldn't ever get this)
        else if (message instanceof MetaMessage) {
            // Print out meta message
            System.out.println();
            System.out.print("Meta MIDI Message {");
            for (int i = 0; i < m.length; i++) {
                System.out.print(" " + m[i]);
            }
            System.out.println("}");
        }

        // Uncast message
        else {
            System.out.println("Unknown MIDI message [");
            for (int i = 0; i < m.length; i++) {
                System.out.print(" " + m[i]);
            }
            System.out.println("]");
        }
    }

    public void close() {
    }

    //--------------------------------------
    // Private methods
    //--------------------------------------

    private void setupMidiInput() {
        try {
            // display MIDI device picker
            final Frame f = new Frame("MIDI Input port: Double-click to select.");
            // See all the midi devices on the system
            final MidiDevice.Info[] info = MidiSystem.getMidiDeviceInfo();
            final List dataList = new List();
            fillFrame(f, dataList, info);
            f.setVisible(true);
            // hanlde double clicking
            MouseListener mouseListener = new MouseAdapter() {
                public void mouseClicked(MouseEvent e) {
                    if (e.getClickCount() == 2) {
                        int index = dataList.getSelectedIndex();
                        setMidiInputSelection(index, info);
                        f.setVisible(false);
                        setupMidiOutput();
                    }
                }
            };
            dataList.addMouseListener(mouseListener);

        } catch (Exception e) {
            System.out.println(e);
            System.exit(0);
        }
    }

    private void setupMidiOutput() {
        try {
            // See all the midi devices on the system
            final MidiDevice.Info[] info = MidiSystem.getMidiDeviceInfo();
            // display MIDI device picker
            final Frame f = new Frame("MIDI Output port: Double-click to select.");
            final List dataList = new List();
            fillFrame(f, dataList, info);
            // hanlde double clicking
            MouseListener mouseListener = new MouseAdapter() {
                public void mouseClicked(MouseEvent e) {
                    if (e.getClickCount() == 2) {
                        int index = dataList.getSelectedIndex();
                        setMidiOutputSelection(index, info);
                        f.setVisible(false);
                        waitingToSetup = false;
                    }
                }
            };
            dataList.addMouseListener(mouseListener);
            f.setVisible(true);
        } catch (Exception e) {
            System.out.println(e);
            System.exit(0);
        }
    }

    // GUI prompt
    private void fillFrame(Frame f, List dataList, MidiDevice.Info[] info) {
        try {
            f.setSize(340, 200);
            f.setLocation(Toolkit.getDefaultToolkit().getScreenSize().width / 2 - 170,
                    Toolkit.getDefaultToolkit().getScreenSize().height / 2 - 100);
            String[] data = new String[info.length];
            data[0] = "" + info[0];
            data[1] = "" + info[1];
            for (int i = 2; i < info.length; i++) {
                data[i] = MidiSystem.getMidiDevice(info[i]).toString();
            }
            for (int i = 0; i < info.length; i++) {
                dataList.add(data[i]);
            }
            ScrollPane scrollPane = new ScrollPane();
            scrollPane.add(dataList);
            f.add(scrollPane);
        } catch (Exception e) {
            System.out.println(e);
            System.exit(0);
        }

    }


    /*
     * Set up the MIDI input
     */
    private void setMidiInputSelection(int inputDeviceID, MidiDevice.Info[] info) {
        try {
            // inut setup
            MidiDevice inputPort = MidiSystem.getMidiDevice(info[inputDeviceID]);
            //System.out.println (inputPort);
            inputPort.open();
            Transmitter t = inputPort.getTransmitter();
            t.setReceiver(this);
        } catch (Exception e) {
            // Oops! Should never get here
            System.out.println("Exception in PlumStone main ()");
            System.out.println(e);
            System.exit(0);
        }
    }

    /*
     * Set up the MIDI output
     */
    private void setMidiOutputSelection(int outputDeviceID, MidiDevice.Info[] info) {
        try {
            // output setup
            MidiDevice outputPort = MidiSystem.getMidiDevice(info[outputDeviceID]);
            outputPort.open();
            this.midiReceiver = outputPort.getReceiver();
            //System.out.println("setMR = " + midiReceiver);
        } catch (Exception e) {
            // Oops! Should never get here
            System.out.println("Exception in PlumStone main ()");
            System.out.println(e);
            System.exit(0);
        }
    }
}